4.06. Вызовы и иерархия
Вызовы и иерархия
Цепочки вызовов — кто кого вызвал
Цепочка вызовов — это последовательность методов или функций, которые вызывают друг друга в процессе выполнения программы.
Каждый вызов функции добавляет новый фрейм в стек вызовов. Стек вызовов отслеживает:
- какая функция вызвала текущую
- какие аргументы были переданы
- куда вернуться после завершения
Пример цепочки вызовов в C#:
public class OrderProcessor
{
public void ProcessOrder(Order order)
{
ValidateOrder(order); // Вызов 1
CalculateTotal(order); // Вызов 2
SaveOrder(order); // Вызов 3
SendConfirmation(order); // Вызов 4
}
private void ValidateOrder(Order order)
{
CheckInventory(order); // Вызов 1.1
ValidateCustomer(order); // Вызов 1.2
}
private void CheckInventory(Order order)
{
// Логика проверки наличия товаров
}
}
Стек вызовов при выполнении CheckInventory:
CheckInventory(order)
↑
ValidateOrder(order)
↑
ProcessOrder(order)
↑
Main()
Пример на Python:
def process_order(order):
validate_order(order) # Вызов 1
calculate_total(order) # Вызов 2
save_order(order) # Вызов 3
def validate_order(order):
check_inventory(order) # Вызов 1.1
validate_customer(order) # Вызов 1.2
def check_inventory(order):
# Логика проверки наличия товаров
pass
Рекурсия — плюсы, минусы, переполнение стека
Рекурсия — это техника, при которой функция вызывает саму себя для решения задачи.
Рекурсивные алгоритмы подходят для задач с естественной рекурсивной структурой:
- обход деревьев и графов
- математические последовательности
- разбор вложенных структур данных
- комбинаторные задачи
Пример факториала:
public int Factorial(int n)
{
if (n <= 1)
return 1;
return n * Factorial(n - 1);
}
// Вызов Factorial(5):
// Factorial(5) = 5 * Factorial(4)
// = 5 * 4 * Factorial(3)
// = 5 * 4 * 3 * Factorial(2)
// = 5 * 4 * 3 * 2 * Factorial(1)
// = 5 * 4 * 3 * 2 * 1 = 120
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
# Вызов factorial(5) строит стек из 5 фреймов
public int factorial(int n) {
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
Преимущества рекурсии:
- естественное выражение рекурсивных задач
- компактный и читаемый код для определённых алгоритмов
- упрощение реализации обхода древовидных структур
Недостатки рекурсии:
- потребление памяти стека пропорционально глубине рекурсии
- риск переполнения стека при большой глубине
- накладные расходы на создание фреймов стека
- сложность отладки по сравнению с итеративными решениями
Пример переполнения стека:
public void InfiniteRecursion()
{
InfiniteRecursion(); // Бесконечная рекурсия без условия выхода
}
// При вызове: StackOverflowException
Итеративная альтернатива факториала:
public int FactorialIterative(int n)
{
int result = 1;
for (int i = 2; i <= n; i++)
{
result *= i;
}
return result;
}
Хвостовая рекурсия (концептуально)
Хвостовая рекурсия — это форма рекурсии, при которой рекурсивный вызов является последней операцией в функции.
Особенность хвостовой рекурсии — компилятор или интерпретатор может оптимизировать её в итеративный цикл, избегая роста стека вызовов.
Пример хвостовой рекурсии на факториале:
public int FactorialTail(int n, int accumulator = 1)
{
if (n <= 1)
return accumulator;
return FactorialTail(n - 1, n * accumulator);
// Рекурсивный вызов — последняя операция
}
def factorial_tail(n, accumulator=1):
if n <= 1:
return accumulator
return factorial_tail(n - 1, n * accumulator)
Сравнение стека вызовов:
Обычная рекурсия (Factorial(5)):
Factorial(5) ожидает результат от Factorial(4)
Factorial(4) ожидает результат от Factorial(3)
Factorial(3) ожидает результат от Factorial(2)
Factorial(2) ожидает результат от Factorial(1)
Factorial(1) возвращает 1
Factorial(2) вычисляет 2 * 1 и возвращает 2
Factorial(3) вычисляет 3 * 2 и возвращает 6
Factorial(4) вычисляет 4 * 6 и возвращает 24
Factorial(5) вычисляет 5 * 24 и возвращает 120
Хвостовая рекурсия (FactorialTail(5, 1)):
FactorialTail(5, 1) → FactorialTail(4, 5)
FactorialTail(4, 5) → FactorialTail(3, 20)
FactorialTail(3, 20) → FactorialTail(2, 60)
FactorialTail(2, 60) → FactorialTail(1, 120)
FactorialTail(1, 120) возвращает 120
При оптимизации хвостовой рекурсии стек не растёт — каждый вызов заменяется переходом с новыми аргументами.
Поддержка оптимизации хвостовой рекурсии:
- F#, Scala, Erlang — полная поддержка на уровне языка
- JavaScript (ES6) — спецификация требует поддержки, но реализация в браузерах ограничена
- C# — нет встроенной оптимизации, но возможна ручная трансформация в цикл
- Python — нет оптимизации, рекомендуются итеративные решения
- Java — нет оптимизации на уровне JVM
Инструменты — дерево вызовов, flame graph, профилирование
Дерево вызовов — визуальное представление иерархии вызовов функций в программе.
Пример дерева вызовов для обработки заказа:
ProcessOrder()
├─ ValidateOrder()
│ ├─ CheckInventory()
│ │ └─ QueryDatabase()
│ └─ ValidateCustomer()
│ └─ CallExternalAPI()
├─ CalculateTotal()
│ └─ ApplyDiscounts()
└─ SaveOrder()
└─ WriteToDatabase()
Flame graph — тепловая карта вызовов, где:
- ширина блока пропорциональна времени выполнения
- вертикальная позиция показывает глубину стека
- цвет может обозначать модуль или тип операции
Пример интерпретации flame graph:
[████████████████████████████████] ProcessOrder() 100ms
[██████████] ValidateOrder() 40ms
[████] CheckInventory() 15ms
[██] QueryDatabase() 8ms
[████] ValidateCustomer() 15ms
[███] CallExternalAPI() 12ms
[████████] CalculateTotal() 30ms
[███] ApplyDiscounts() 12ms
[████] SaveOrder() 15ms
[███] WriteToDatabase() 12ms
Инструменты для анализа вызовов:
| Инструмент | Платформа | Особенности |
|---|---|---|
| Visual Studio Profiler | .NET | Интеграция с IDE, call tree view |
| dotTrace | .NET | Flame graphs, timeline analysis |
| YourKit | Java | CPU/memory profiling, call chains |
| Async Profiler | JVM | Low-overhead, flame graphs |
| py-spy | Python | Sampling profiler, flame graphs без модификации кода |
| Chrome DevTools | JavaScript | Performance tab, call tree |
| Perf | Linux | Системный профилировщик, поддержка flame graphs |
Пример использования профилировщика в .NET:
// Запуск профилирования через командную строку
dotnet trace collect --profile cpu-sampling -o trace.nettrace -- myapp.dll
// Анализ результата в SpeedScope или PerfView
Пример использования py-spy для Python:
# Запуск профилирования без изменения кода
py-spy record -o profile.svg -- python myapp.py
# Режим top для реального времени
py-spy top --pid 12345
Обратные вызовы
Обратный вызов (callback) — это функция, передаваемая как аргумент другой функции и вызываемая по завершении определённого события или операции.
Обратные вызовы применяются для:
- асинхронных операций
- обработки событий
- реализации стратегий и политик
- расширения поведения без изменения исходного кода
Пример обратного вызова в JavaScript:
// Синхронный обратный вызов
function processData(data, callback) {
const result = data.map(item => item * 2);
callback(result);
}
processData([1, 2, 3], (result) => {
console.log(result); // [2, 4, 6]
});
// Асинхронный обратный вызов
function fetchData(url, onSuccess, onError) {
fetch(url)
.then(response => response.json())
.then(data => onSuccess(data))
.catch(error => onError(error));
}
fetchData(
'https://api.example.com/data',
(data) => console.log('Данные получены:', data),
(error) => console.error('Ошибка:', error)
);
Пример на C# с делегатами:
public delegate void DataProcessedHandler(int[] result);
public class DataProcessor
{
public void Process(int[] data, DataProcessedHandler callback)
{
var result = data.Select(x => x * 2).ToArray();
callback(result);
}
}
// Использование
var processor = new DataProcessor();
processor.Process(new[] { 1, 2, 3 }, result => {
Console.WriteLine(string.Join(", ", result)); // 2, 4, 6
});
Пример на Python с функциями высшего порядка:
def process_data(data, callback):
result = [x * 2 for x in data]
callback(result)
def print_result(result):
print(result)
process_data([1, 2, 3], print_result) # [2, 4, 6]
# Использование лямбды
process_data([1, 2, 3], lambda r: print(f"Результат: {r}"))
Проблемы обратных вызовов:
- callback hell — вложенность обратных вызовов затрудняет чтение кода
- сложность обработки ошибок в асинхронных цепочках
- трудности с отменой операций
- потеря контекста выполнения
Современные альтернативы обратным вызовам:
- Промисы (Promises) — цепочки асинхронных операций
- Асинхронные функции (async/await) — синхронный стиль для асинхронного кода
- Реактивные расширения (Rx) — потоки событий с операторами трансформации